gtk: Add a way to do event capture
authorCarlos Garcia Campos <cgarcia@igalia.com>
Tue, 8 Feb 2011 13:49:31 +0000 (14:49 +0100)
committerMatthias Clasen <mclasen@redhat.com>
Thu, 1 Mar 2012 21:25:21 +0000 (16:25 -0500)
This patch adds a capture phase to GTK+'s event propagation
model. Events are first propagated from the toplevel (or the
grab widget, if a grab is in place) down to the target widget
 and then back up. The second phase is using the existing
::event signal, the new capture phase is using a private
API instead of a public signal for now.

This mechanism can be used in many places where we currently
have to prevent child widgets from getting events by putting
an input-only window over them. It will also be used to implement
kinetic scrolling in subsequent patches.

http://bugzilla.gnome.org/show_bug.cgi?id=641836

We automatically request more motion events in behalf of
the original widget if it listens to motion hints. So
the capturing widget doesn't need to handle such
implementation details.

We are not making event capture part of the public API for 3.4,
which is why there is no ::captured-event signal.

gtk/gtkmain.c
gtk/gtkprivate.h
gtk/gtkwidget.c
gtk/gtkwidget.h
gtk/gtkwidgetprivate.h

index c0cdabd9c88ae32056d39de5ab9ae6a6dde5d4fd..68ecbfeb65f754727b1d3fa298e0a0b2fdae9846 100644 (file)
@@ -1471,6 +1471,7 @@ gtk_main_do_event (GdkEvent *event)
 {
   GtkWidget *event_widget;
   GtkWidget *grab_widget = NULL;
+  GtkWidget *topmost_widget = NULL;
   GtkWindowGroup *window_group;
   GdkEvent *rewritten_event = NULL;
   GdkDevice *device;
@@ -1530,6 +1531,14 @@ gtk_main_do_event (GdkEvent *event)
   if (!grab_widget)
     grab_widget = gtk_window_group_get_current_grab (window_group);
 
+  /* Find out the topmost widget where captured event propagation
+   * should start, which is the widget holding the GTK+ grab
+   * if any, otherwise it's left NULL and events are emitted
+   * from the toplevel (or topmost parentless parent).
+   */
+  if (grab_widget)
+    topmost_widget = grab_widget;
+
   /* If the grab widget is an ancestor of the event widget
    * then we send the event to the original event widget.
    * This is the key to implementing modality.
@@ -1626,14 +1635,16 @@ gtk_main_do_event (GdkEvent *event)
     case GDK_WINDOW_STATE:
     case GDK_GRAB_BROKEN:
     case GDK_DAMAGE:
-      gtk_widget_event (event_widget, event);
+      if (!_gtk_widget_captured_event (event_widget, event))
+        gtk_widget_event (event_widget, event);
       break;
 
     case GDK_SCROLL:
     case GDK_BUTTON_PRESS:
     case GDK_2BUTTON_PRESS:
     case GDK_3BUTTON_PRESS:
-      gtk_propagate_event (grab_widget, event);
+      if (!_gtk_propagate_captured_event (grab_widget, event, topmost_widget))
+        gtk_propagate_event (grab_widget, event);
       break;
 
     case GDK_KEY_PRESS:
@@ -1682,7 +1693,8 @@ gtk_main_do_event (GdkEvent *event)
     case GDK_BUTTON_RELEASE:
     case GDK_PROXIMITY_IN:
     case GDK_PROXIMITY_OUT:
-      gtk_propagate_event (grab_widget, event);
+      if (!_gtk_propagate_captured_event (grab_widget, event, topmost_widget))
+        gtk_propagate_event (grab_widget, event);
       break;
 
     case GDK_ENTER_NOTIFY:
@@ -1691,7 +1703,8 @@ gtk_main_do_event (GdkEvent *event)
         _gtk_widget_set_device_window (event_widget,
                                        gdk_event_get_device (event),
                                        event->any.window);
-      if (gtk_widget_is_sensitive (grab_widget))
+      if (gtk_widget_is_sensitive (grab_widget) &&
+          !_gtk_propagate_captured_event (grab_widget, event, topmost_widget))
         gtk_widget_event (grab_widget, event);
       break;
 
@@ -1701,7 +1714,8 @@ gtk_main_do_event (GdkEvent *event)
         _gtk_widget_set_device_window (event_widget,
                                        gdk_event_get_device (event),
                                        NULL);
-      if (gtk_widget_is_sensitive (grab_widget))
+      if (gtk_widget_is_sensitive (grab_widget) &&
+          !_gtk_propagate_captured_event (grab_widget, event, topmost_widget))
         gtk_widget_event (grab_widget, event);
       break;
 
@@ -2331,45 +2345,96 @@ gtk_get_event_widget (GdkEvent *event)
   return widget;
 }
 
-/**
- * gtk_propagate_event:
- * @widget: a #GtkWidget
- * @event: an event
- *
- * Sends an event to a widget, propagating the event to parent widgets
- * if the event remains unhandled.
- *
- * Events received by GTK+ from GDK normally begin in gtk_main_do_event().
- * Depending on the type of event, existence of modal dialogs, grabs, etc.,
- * the event may be propagated; if so, this function is used.
- *
- * gtk_propagate_event() calls gtk_widget_event() on each widget it
- * decides to send the event to. So gtk_widget_event() is the lowest-level
- * function; it simply emits the #GtkWidget::event and possibly an
- * event-specific signal on a widget. gtk_propagate_event() is a bit
- * higher-level, and gtk_main_do_event() is the highest level.
- *
- * All that said, you most likely don't want to use any of these
- * functions; synthesizing events is rarely needed. There are almost
- * certainly better ways to achieve your goals. For example, use
- * gdk_window_invalidate_rect() or gtk_widget_queue_draw() instead
- * of making up expose events.
- */
-void
-gtk_propagate_event (GtkWidget *widget,
-                     GdkEvent  *event)
+static gboolean
+propagate_event_up (GtkWidget *widget,
+                    GdkEvent  *event,
+                    GtkWidget *topmost)
 {
-  gint handled_event;
+  gboolean handled_event = FALSE;
 
-  g_return_if_fail (GTK_IS_WIDGET (widget));
-  g_return_if_fail (event != NULL);
+  /* Propagate event up the widget tree so that
+   * parents can see the button and motion
+   * events of the children.
+   */
+  while (TRUE)
+    {
+      GtkWidget *tmp;
+
+      g_object_ref (widget);
+
+      /* Scroll events are special cased here because it
+       * feels wrong when scrolling a GtkViewport, say,
+       * to have children of the viewport eat the scroll
+       * event
+       */
+      if (!gtk_widget_is_sensitive (widget))
+        handled_event = event->type != GDK_SCROLL;
+      else
+        handled_event = gtk_widget_event (widget, event);
+
+      tmp = gtk_widget_get_parent (widget);
+      g_object_unref (widget);
+
+      if (widget == topmost)
+        break;
+
+      widget = tmp;
+
+      if (handled_event || !widget)
+        break;
+    }
+
+  return handled_event;
+}
+
+static gboolean
+propagate_event_down (GtkWidget *widget,
+                      GdkEvent  *event,
+                      GtkWidget *topmost)
+{
+  gint handled_event = FALSE;
+  GList *widgets = NULL;
+  GList *l;
+
+  widgets = g_list_prepend (widgets, g_object_ref (widget));
+  while (widget && widget != topmost)
+    {
+      widget = gtk_widget_get_parent (widget);
+      if (!widget)
+        break;
+
+      widgets = g_list_prepend (widgets, g_object_ref (widget));
+
+      if (widget == topmost)
+        break;
+    }
+
+  for (l = widgets; l && !handled_event; l = g_list_next (l))
+    {
+      widget = (GtkWidget *)l->data;
 
-  handled_event = FALSE;
+      if (!gtk_widget_is_sensitive (widget))
+        handled_event = TRUE;
+      else
+        handled_event = _gtk_widget_captured_event (widget, event);
+    }
+  g_list_free_full (widgets, (GDestroyNotify)g_object_unref);
 
-  g_object_ref (widget);
+  return handled_event;
+}
 
-  if ((event->type == GDK_KEY_PRESS) ||
-      (event->type == GDK_KEY_RELEASE))
+static gboolean
+propagate_event (GtkWidget *widget,
+                 GdkEvent  *event,
+                 gboolean   captured,
+                 GtkWidget *topmost)
+{
+  gboolean handled_event = FALSE;
+  gboolean (* propagate_func) (GtkWidget *widget, GdkEvent  *event);
+
+  propagate_func = captured ? _gtk_widget_captured_event : gtk_widget_event;
+
+  if (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE)
     {
       /* Only send key events within Window widgets to the Window
        * The Window widget will in turn pass the
@@ -2381,11 +2446,12 @@ gtk_propagate_event (GtkWidget *widget,
       window = gtk_widget_get_toplevel (widget);
       if (GTK_IS_WINDOW (window))
         {
+          g_object_ref (widget);
           /* If there is a grab within the window, give the grab widget
            * a first crack at the key event
            */
           if (widget != window && gtk_widget_has_grab (widget))
-            handled_event = gtk_widget_event (widget, event);
+            handled_event = propagate_func (widget, event);
 
           if (!handled_event)
             {
@@ -2393,45 +2459,59 @@ gtk_propagate_event (GtkWidget *widget,
               if (GTK_IS_WINDOW (window))
                 {
                   if (gtk_widget_is_sensitive (window))
-                    gtk_widget_event (window, event);
+                    handled_event = propagate_func (window, event);
                 }
             }
 
-          handled_event = TRUE; /* don't send to widget */
+          g_object_unref (widget);
+          return handled_event;
         }
     }
 
-  /* Other events get propagated up the widget tree
-   * so that parents can see the button and motion
-   * events of the children.
-   */
-  if (!handled_event)
-    {
-      while (TRUE)
-        {
-          GtkWidget *tmp;
-
-          /* Scroll events are special cased here because it
-           * feels wrong when scrolling a GtkViewport, say,
-           * to have children of the viewport eat the scroll
-           * event
-           */
-          if (!gtk_widget_is_sensitive (widget))
-            handled_event = event->type != GDK_SCROLL;
-          else
-            handled_event = gtk_widget_event (widget, event);
+  /* Other events get propagated up/down the widget tree */
+  return captured ?
+    propagate_event_down (widget, event, topmost) :
+    propagate_event_up (widget, event, topmost);
+}
 
-          tmp = gtk_widget_get_parent (widget);
-          g_object_unref (widget);
+/**
+ * gtk_propagate_event:
+ * @widget: a #GtkWidget
+ * @event: an event
+ *
+ * Sends an event to a widget, propagating the event to parent widgets
+ * if the event remains unhandled.
+ *
+ * Events received by GTK+ from GDK normally begin in gtk_main_do_event().
+ * Depending on the type of event, existence of modal dialogs, grabs, etc.,
+ * the event may be propagated; if so, this function is used.
+ *
+ * gtk_propagate_event() calls gtk_widget_event() on each widget it
+ * decides to send the event to. So gtk_widget_event() is the lowest-level
+ * function; it simply emits the #GtkWidget::event and possibly an
+ * event-specific signal on a widget. gtk_propagate_event() is a bit
+ * higher-level, and gtk_main_do_event() is the highest level.
+ *
+ * All that said, you most likely don't want to use any of these
+ * functions; synthesizing events is rarely needed. There are almost
+ * certainly better ways to achieve your goals. For example, use
+ * gdk_window_invalidate_rect() or gtk_widget_queue_draw() instead
+ * of making up expose events.
+ */
+void
+gtk_propagate_event (GtkWidget *widget,
+                     GdkEvent  *event)
+{
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+  g_return_if_fail (event != NULL);
 
-          widget = tmp;
+  propagate_event (widget, event, FALSE, NULL);
+}
 
-          if (!handled_event && widget)
-            g_object_ref (widget);
-          else
-            break;
-        }
-    }
-  else
-    g_object_unref (widget);
+gboolean
+_gtk_propagate_captured_event (GtkWidget *widget,
+                               GdkEvent  *event,
+                               GtkWidget *topmost)
+{
+  return propagate_event (widget, event, TRUE, topmost);
 }
index 63860a21a0fbd48167aab2ce6e6ee63351b41f06..9114bd08b2790473ad5b1a3f1214baa92d2bae92 100644 (file)
@@ -72,6 +72,10 @@ gboolean _gtk_translate_keyboard_accel_state   (GdkKeymap       *keymap,
                                                 gint            *level,
                                                 GdkModifierType *consumed_modifiers);
 
+gboolean        _gtk_propagate_captured_event  (GtkWidget       *widget,
+                                                GdkEvent        *event,
+                                                GtkWidget       *topmost);
+
 G_END_DECLS
 
 #endif /* __GTK_PRIVATE_H__ */
index c7406fc882701a3e6de16de9f3bfd9cd01c6308e..23ab0d8e62d01d6602acee64ebc87f6cd8e04bf7 100644 (file)
@@ -706,6 +706,7 @@ static void gtk_widget_set_device_enabled_internal (GtkWidget *widget,
                                                     GdkDevice *device,
                                                     gboolean   recurse,
                                                     gboolean   enabled);
+static gboolean event_window_is_still_viewable (GdkEvent *event);
 
 /* --- variables --- */
 static gpointer         gtk_widget_parent_class = NULL;
@@ -5951,6 +5952,59 @@ gtk_widget_event (GtkWidget *widget,
   return gtk_widget_event_internal (widget, event);
 }
 
+void
+_gtk_widget_set_captured_event_handler (GtkWidget               *widget,
+                                        GtkCapturedEventHandler  callback)
+{
+  g_object_set_data (G_OBJECT (widget), "captured-event-handler", callback);
+}
+
+gboolean
+_gtk_widget_captured_event (GtkWidget *widget,
+                            GdkEvent  *event)
+{
+  gboolean return_val = FALSE;
+  GtkCapturedEventHandler handler;
+
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), TRUE);
+  g_return_val_if_fail (WIDGET_REALIZED_FOR_EVENT (widget, event), TRUE);
+
+  if (event->type == GDK_EXPOSE)
+    {
+      g_warning ("Events of type GDK_EXPOSE cannot be synthesized. To get "
+                "the same effect, call gdk_window_invalidate_rect/region(), "
+                "followed by gdk_window_process_updates().");
+      return TRUE;
+    }
+
+  if (!event_window_is_still_viewable (event))
+    return TRUE;
+
+  handler = g_object_get_data (G_OBJECT (widget), "captured-event-handler");
+  if (!handler)
+    return FALSE;
+
+  g_object_ref (widget);
+
+  return_val = handler (widget, event);
+  return_val |= !WIDGET_REALIZED_FOR_EVENT (widget, event);
+
+  /* The widget that was originally to receive the event
+   * handles motion hints, but the capturing widget might
+   * not, so ensure we get further motion events.
+   */
+  if (return_val &&
+      event->type == GDK_MOTION_NOTIFY &&
+      event->motion.is_hint &&
+      (gdk_window_get_events (event->any.window) &
+       GDK_POINTER_MOTION_HINT_MASK) != 0)
+    gdk_event_request_motions (&event->motion);
+
+  g_object_unref (widget);
+
+  return return_val;
+}
+
 /* Returns TRUE if a translation should be done */
 gboolean
 _gtk_widget_get_translation_to_window (GtkWidget      *widget,
index 4fd7fce08616d0513c141ab3ffe9ce9ccb39e82c..5a4cdbcbffcd721c29e4a5fabc31ff93c9fc4a8b 100644 (file)
@@ -437,7 +437,6 @@ struct _GtkWidgetClass
   void (*_gtk_reserved5) (void);
   void (*_gtk_reserved6) (void);
   void (*_gtk_reserved7) (void);
-  void (*_gtk_reserved8) (void);
 };
 
 struct _GtkWidgetAuxInfo
index 58dd0e0861815a0f00f71afc0a7bd61768bbb7b8..9774c13fe8495c9d04077fc81d0cfa3617d55008 100644 (file)
@@ -165,6 +165,13 @@ GtkStyle *        _gtk_widget_get_style                    (GtkWidget *widget);
 void              _gtk_widget_set_style                    (GtkWidget *widget,
                                                             GtkStyle  *style);
 
+typedef gboolean (*GtkCapturedEventHandler) (GtkWidget *widget, GdkEvent *event);
+
+void              _gtk_widget_set_captured_event_handler (GtkWidget               *widget,
+                                                          GtkCapturedEventHandler  handler);
+
+gboolean          _gtk_widget_captured_event               (GtkWidget *widget,
+                                                            GdkEvent  *event);
 
 G_END_DECLS